/**
 * @Author: hogan
 * @Date: 2021/6/21
 * @General Documentation: 爬虫部分代码
 **/
package spider

import (
	"fmt"
	"github.com/axgle/mahonia"
	"io/ioutil"
	"net/http"
	lurl "net/url"
	"os"
	"path"
	"regexp"
	"strings"
	"sync"
	"time"
	"unicode/utf8"
)
import (
	"cn.hogan/modetest/src/config"
	"cn.hogan/modetest/src/logger"
)

var (
	maps = sync.Map{} // 去重的集合
	cnf   config.ReturnConfig
)

// 爬虫任务结构体
type SpiderWork struct {
	Url  string // 地址
	Deep int    // 深度
}

/**
 * @Description: 根据url获取参数
 * @Param: url 网页地址
 * @Returns: body 内容
 **/
func GetHtml(url string) string {
	logger.Info("crawling " + url)
	// 创建请求
	req, err := http.NewRequest("GET", url, nil)

	logger.Info("return ")
	//为请求对象NewRequest设置请求头
	req.Header.Add("Content-Type", "text/html;charset=utf-8")
	req.Header.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1)"+
		" AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36")

	// 创建HTTP客户端
	client := &http.Client{
		Timeout: time.Second * time.Duration(cnf.Config.CrawlTimeout), // 设置超时时间
	}
	// 发出请求
	resp, err := client.Do(req)

	if err != nil {
		logger.Error("Http get err:", err, ",url:", url)
		return ""
	}
	if resp.StatusCode != 200 {
		logger.Error("Http status code:", resp.StatusCode, ",url: ", url)
		return ""
	}
	// 关闭
	defer resp.Body.Close()
	// 读取HTTP响应正文
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		logger.Error("Read error", err)
		return ""
	}
	// 将byte的body转换成string
	res_body := string(body)
	// 判断编码
	if !utf8.Valid(body) {
		// 编码转换，如编码不正确可转成指定编码
		srcCoder := mahonia.NewEncoder("utf-8")
		res_body = srcCoder.ConvertString(res_body)
	}
	return res_body
}

/**
 * @Description: 获取页面的url
 * @Param: 传入html字符串源码
 * @Returns:
 **/
func GetPageLink(html string, work SpiderWork) []SpiderWork {
	// 提取出所有a标签超链接
	relink := `<a[\s\S]+?href="([\s\S]+?)"`
	reg := regexp.MustCompile(relink)
	rlts := reg.FindAllStringSubmatch(html, -1)

	// 筛选符合要求的连接
	ms := regexp.MustCompile(cnf.Config.TargetUrl)
	var args = make([]SpiderWork, 0)   // 初始化
	parse, err := lurl.Parse(work.Url) // url解析
	if err != nil {
		logger.Error(err)
		panic(err)
	}
	for _, value := range rlts {
		// 判断是否符合正则表达式
		tmpUrl := value[1]
		if ms.MatchString(tmpUrl) {
			// 如果符合表达式的
			// 是否是http
			if strings.Index(tmpUrl, "http") != 0 {
				// 不是https，那么就是相对路径
				if strings.Index(tmpUrl, parse.Host) != 0 {
					// 相对地址
					tmpUrl = parse.Scheme + "://" + parse.Host + "/" + tmpUrl
				} else {
					tmpUrl = parse.Scheme + "://" + tmpUrl
				}
			}
			// 添加url
			_work := SpiderWork{Url: tmpUrl, Deep: work.Deep + 1}
			// 判断是否存在
			if _, ok := maps.Load(tmpUrl); !ok {
				maps.Store(tmpUrl, "")
				args = append(args, _work)
			}
		}
	}
	return args
}

/**
 * @Description:深度控制
 * @Param: work 爬取队列
 * @Returns:
 **/
func DeepCtl(work SpiderWork, wg *sync.WaitGroup) {
	// 判断是否到达最大深度
	if work.Deep > cnf.Config.MaxDepth {
		return
	}

	// 没有的到达最大深度
	html := GetHtml(work.Url) // 获取页面源码

	// 是否符合要求的连接
	regExp := regexp.MustCompile(cnf.Config.TargetUrl)
	ms := regExp.MatchString(work.Url)
	fmt.Println(work)
	if ms && html != "" {
		// 下载页面
		wg.Add(1)
		go DownLoadPage(html, work.Url, wg)
	}

	links := GetPageLink(html, work) // 获取本页面的所有符合条件的url

	// 执行递归向下解析
	for _, _work := range links {
		DeepCtl(_work, wg)
	}

}

/**
 * @Description: 根据url获取下载网页内容保存本地
 * @Param: url 页面的url，body页面的内容
 * @Returns: nil
 **/
func DownLoadPage(body string, url string, wg *sync.WaitGroup) {
	defer wg.Done()

	logger.Info("save url: ", url)

	// 编解码、配置目录
	outputDir := cnf.Config.OutputDirectory
	filename := path.Join(outputDir, lurl.QueryEscape(url))
	f, err := os.Create(filename)
	// 创建失败
	if err != nil {
		logger.Error("File creation failed, err:", err)
		return
	}
	// 写入文件
	f.WriteString(body)
	// 关闭文件
	defer f.Close()

}

func Start(urls []string, conf config.ReturnConfig, wg *sync.WaitGroup) {
	cnf = conf
	for _, v := range urls {
		var work = SpiderWork{Url: v, Deep: 0}
		DeepCtl(work, wg)
	}
}
